﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Diagnostics;
using Microsoft.Practices.EnterpriseLibrary.Logging;
using Microsoft.Practices.Unity.Utility;

namespace MSAS.DIP.Common
{
    public enum PerfCounterGroup
    {
        All
    }

    /// <summary>
    /// Wrapper for a performance counter that allows for grouping and enabled/disabled
    /// toggling.
    /// </summary>
    public class PerfCounterInfo
    {
        private PerfCounterId m_perfCounterId;
        private PerfCounterGroup m_group;
        private PerformanceCounter m_perfCounter;
        private bool m_isEnabled;

        public PerfCounterInfo(
            PerfCounterId perfCounterId,
            PerformanceCounter perfCounter, 
            PerfCounterGroup group)
        {
            m_perfCounterId = perfCounterId;
            m_group = group;
            m_perfCounter = perfCounter;
            m_isEnabled = true;
        }

        /// <summary>
        /// Increments the performance counter if it is active
        /// </summary>
        public void Increment()
        {
            if (m_isEnabled)
            {
                m_perfCounter.Increment();
            }
        }

        /// <summary>
        /// Increments the performance counter if it is active
        /// </summary>
        /// <param name="value">Value to increment by</param>
        public void IncrementBy(long value)
        {
            if (m_isEnabled)
            {
                m_perfCounter.IncrementBy(value);
            }
        }

        /// <summary>
        /// The ID of the performance counter
        /// </summary>
        public PerfCounterId Id
        {
            get
            {
                return m_perfCounterId;
            }
        }

        /// <summary>
        /// The group the performance counter belongs to
        /// </summary>
        public PerfCounterGroup Group
        {
            get
            {
                return m_group;
            }
        }

        public PerformanceCounter PerfCounter
        {
            get
            {
                return m_perfCounter;
            }
        }

        /// <summary>
        /// Determines if the performance counter is enabled or disabled
        /// </summary>
        public bool IsEnabled
        {
            get
            {
                return m_isEnabled;
            }
            set
            {
                m_isEnabled = value;
            }
        }
    }

    /// <summary>
    /// The performance counters in DIP Host. Note that order must match PerfCounters.CounterNames
    /// </summary>
    public enum PerfCounterId
    {
        TimeToCreateWorkUnitAverage = 0,
        TimeToCreateWorkUnitBase,
        WorkUnitsCreatedTotal,
        ObjectsPerWorkUnitAverage,
        ObjectsPerWorkUnitBase,
        TimeToImportObjectAverage,
        TimeToImportObjectBase,
        ObjectsImportedTotal,
        TimeToImportWorkUnitAverage,
        TimeToImportWorkUnitBase,
        ObjectsImportedPerWorkUnitAverage,
        ObjectsImportedPerWorkUnitBase,
        TimeToNormalizeImportedObjectAverage,
        TimeToNormalizeImportedObjectBase,
        TimeToNormalizeWorkUnitAverage,
        TimeToNormalizeWorkUnitBase,
        TimeToTransferWorkUnitAverage,
        TimeToTransferWorkUnitBase,
        TimeToGetWorkUnitsAverage,
        TimeToGetWorkUnitsBase,
        WorkUnitsTransferredTotal,
        WorkUnitsImportedTotal,
        WorkUnitSizeAverage,
        WorkUnitSizeBase,
    }

    public static class PerfCounters
    {
        private const string LoggingCategory = "PerfCounters";
        
        private const string PerfCounterCategory = "MRS.DipHost";
        private const string DefaultInstanceName = "Default";

        private static readonly string MajorVersion = Assembly.GetExecutingAssembly().GetName().Version.Major.ToString();
        private static readonly string ServiceNameDotVersionDot = string.Format("{0}.V{1}.", "DipHost", MajorVersion);
        
        /// <summary>
        /// Performance counter names. Note that ServiceNameDotVersion is prepended during initialization.
        /// Order maps to PerfCounterId enum
        /// </summary>
        private static string[] CounterNames = 
        {
            "TimeToCreateWorkUnitAverage",
            "TimeToCreateWorkUnitBase",
            "WorkUnitsCreatedTotal",
            "ObjectsPerWorkUnitAverage",
            "ObjectsPerWorkUnitBase",
            "TimeToImportObjectAverage",
            "TimeToImportObjectBase",
            "ObjectsImportedTotal",
            "TimeToImportWorkUnitAverage",
            "TimeToImportWorkUnitBase",
            "ObjectsImportedPerWorkUnitAverage",
            "ObjectsImportedPerWorkUnitBase",
            "TimeToNormalizeImportedObjectAverage",
            "TimeToNormalizeImportedObjectBase",
            "TimeToNormalizeWorkUnitAverage",
            "TimeToNormalizeWorkUnitBase",
            "TimeToTransferWorkUnitAverage",
            "TimeToTransferWorkUnitBase",
            "TimeToGetWorkUnitsAverage",
            "TimeToGetWorkUnitsBase",
            "WorkUnitsTransferredTotal",
            "WorkUnitsImportedTotal",
            "WorkUnitSizeAverage",
            "WorkUnitSizeBase",
        };

        /// <summary>
        /// The list of performance counters. Keeping this simple and just using a list,
        /// iterating over it to find the counter needed. In the future if this becomes an
        /// issue, we can take a more complex approach using a Dictionary with a complex
        /// key of PerfCounterId which would be a pair of values: PerfCounter as an int, and
        /// instance name as a string. You could also then index PerfCounterInfo objects by
        /// PerfCounterGroup, and pull group out of PerfCounterInfo.
        /// </summary>
        private static List<PerfCounterInfo> s_perfCounters;

        #region Initialization

        /// <summary>
        /// Initializes the performance counters. Must be called before any counters are incremented.
        /// </summary>
        /// <param name="dataProviderNames">The list of data providers supported in any way by this DIP host</param>
        public static void Initialize(List<string> dataProviderNames)
        {
            if (!Properties.Settings.Default.PerformanceCountersEnabled)
            {
                return;
            }

            //
            // Ensure the perf counter category exists
            //

            if (!PerformanceCounterCategory.Exists(PerfCounterCategory))
            {
                string message = String.Format("The performance counter category {0} does not exist", PerfCounterCategory);
                Logger.Write(new LogEntry()
                {
                    Message = message,
                    Categories = { LoggingCategory, DipLoggingCategory.EventLog },
                    Severity = TraceEventType.Critical,
                    EventId = (int)EventIdType.CriticalPerfCountersCategoryNotFound
                });
                throw new ApplicationException(message);
            }


            //
            // We use Stopwatch.ElapsedTicks for averageTimer32 counters. For this to work (i.e. for it to use 
            // QueryPerformanceCounter under the hood) the system needs to have support for high resolution timers
            //

            if (!Stopwatch.IsHighResolution)
            {
                string message = "The hardware of the computer does not support a high resolution timer used for performance counters.";
                Logger.Write(new LogEntry()
                {
                    Message = message,
                    Categories = { LoggingCategory, DipLoggingCategory.EventLog },
                    Severity = TraceEventType.Error,
                    EventId = (int)EventIdType.ErrorHighResolutionTimerNotSupported
                });
            }

            s_perfCounters = new List<PerfCounterInfo>();

            //
            // Create the performance counters. 
            //

            for (int i = 0; i < CounterNames.Length; i++)
            {
                string fullCounterName = String.Format(
                    "{0}{1}", 
                    ServiceNameDotVersionDot, 
                    CounterNames[i]);

                PerfCounterInfo perfCounterInfo = new PerfCounterInfo(
                    (PerfCounterId)i,
                    CreatePerformanceCounter(
                        PerfCounterCategory,
                        fullCounterName,
                        DefaultInstanceName),
                    PerfCounterGroup.All);

                s_perfCounters.Add(perfCounterInfo);

                foreach (string dataProviderName in dataProviderNames)
                {
                    PerfCounterInfo dataProviderInstance = new PerfCounterInfo(
                        (PerfCounterId)i,
                        CreatePerformanceCounter(
                            PerfCounterCategory,
                            fullCounterName,
                            dataProviderName),
                        PerfCounterGroup.All);

                    s_perfCounters.Add(dataProviderInstance);
                }
            }
        }

        /// <summary>
        /// Creates a read/write PerformanceCounter object with the given properties
        /// </summary>
        /// <param name="category">Category the performance counter belongs to</param>
        /// <param name="counterName">The name of the performance counter</param>
        /// <param name="instanceName">The instance name for the counter</param>
        /// <returns>A read/write PerformanceCounter</returns>
        private static PerformanceCounter CreatePerformanceCounter(
            string category, 
            string counterName,
            string instanceName)
        {
            try
            {
                return new PerformanceCounter(
                    category,
                    counterName,
                    instanceName,
                    false);
            }
            catch (Exception ex)
            {
                Logger.Write(new LogEntry()
                {
                    Message = String.Format(
                        "The read/write performance counter '{1}' in category '{2}'  cannot be created:{0}{3}",
                        Environment.NewLine,
                        counterName,
                        category,
                        ex.ToString()),
                    Severity = TraceEventType.Critical,
                    EventId = (int)EventIdType.CriticalPerfCounterCannotBeCreated,
                    Categories = { LoggingCategory, DipLoggingCategory.EventLog }
                });

                throw;
            }
        }

        #endregion

        #region Counter Helpers

        /// <summary>
        /// Finds the requested PerfCounterInfo object
        /// </summary>
        /// <param name="perfCounterId">The ID of the performance counter to look for</param>
        /// <param name="instanceName">The instance name of the performance counter to look for</param>
        /// <returns>PerfCounterInfo object with the given ID and instance name</returns>
        private static PerfCounterInfo GetPerfCounterInfo(
            PerfCounterId perfCounterId,
            string instanceName)
        {
            foreach (PerfCounterInfo perfCounterInfo in s_perfCounters)
            {
                if (perfCounterInfo.Id == perfCounterId &&
                    perfCounterInfo.PerfCounter.InstanceName == instanceName)
                {
                    return perfCounterInfo;
                }
            }

            return null;
        }

        private static void IncrementDefaultAndInstance(
            PerfCounterId perfCounterId,
            string instanceName)
        {
            IncrementDefaultAndInstanceBy(perfCounterId, instanceName, 1);
        }

        /// <summary>
        /// Increments the "default" instance and the named instance by the given value. Does nothing
        /// if performance counters are globally disabled.
        /// </summary>
        /// <param name="perfCounterId">The ID of the counter to increment</param>
        /// <param name="instanceName">The named instance to increment</param>
        /// <param name="value">The value to increment by</param>
        private static void IncrementDefaultAndInstanceBy(
            PerfCounterId perfCounterId,
            string instanceName,
            long value)
        {
            if (!Properties.Settings.Default.PerformanceCountersEnabled)
            {
                return;
            }

            PerfCounterInfo p1 = GetPerfCounterInfo(perfCounterId, DefaultInstanceName);
            PerfCounterInfo p2 = GetPerfCounterInfo(perfCounterId, instanceName);
            p1.IncrementBy(value);
            if (p2 != null)
            {
                //
                // This counter is a per-data provider one
                //
                p2.IncrementBy(value);
            }
        }

        /// <summary>
        /// Increments the counter by the given value, and increments its associated base counter by one.
        /// This is done for the "default" instance and the given named instance. Does nothing if the
        /// performance counters are globally disabled.
        /// </summary>
        /// <param name="perfCounterId">The ID of the counter to increment by the given value</param>
        /// <param name="baseCounter">The ID of the base counter to increment by one</param>
        /// <param name="instanceName">The named instance to increment</param>
        /// <param name="value">The value to increment by</param>
        private static void IncrementCounterWithBase(
            PerfCounterId perfCounterId,
            PerfCounterId baseCounter,
            string instanceName,
            long value)
        {
            if (!Properties.Settings.Default.PerformanceCountersEnabled)
            {
                return;
            }

            PerfCounterInfo p1 = GetPerfCounterInfo(perfCounterId, DefaultInstanceName);
            PerfCounterInfo p2 = GetPerfCounterInfo(baseCounter, DefaultInstanceName);
            PerfCounterInfo p3 = GetPerfCounterInfo(perfCounterId, instanceName);
            PerfCounterInfo p4 = GetPerfCounterInfo(baseCounter, instanceName);

            //
            // Note: apparently it is important to increment the averageBase counter 
            // immediately after the averageTimer32 or averageCount64 
            //

            p1.IncrementBy(value);
            p2.Increment();

            if (p3 != null && p4 != null)
            {
                //
                // This counter is a per-data provider one
                //
                p3.IncrementBy(value);
                p4.Increment();
            }
        }

        /// <summary>
        /// Logs an error when a performance counter cannot be incremented
        /// </summary>
        /// <param name="ex">The exception that occurred while trying to increment the counter</param>
        private static void LogPerfCounterIncrementError(Exception ex)
        {
            Logger.Write(new LogEntry()
            {
                Message = String.Format("A performance counter cannot be incremented:{0}{1}", Environment.NewLine, ex.ToString()),
                Severity = TraceEventType.Error,
                EventId = (int)EventIdType.ErrorPerfCounterCannotBeIncremented,
                Categories = { LoggingCategory }
            });
        }

        #endregion

        #region Actions to change counters

        public static void CreatedWorkUnits(
            string dataProviderName, 
            int numberOfWorkUnitsCreated,
            long ticks)
        {
            try
            {
                IncrementDefaultAndInstanceBy(
                    PerfCounterId.WorkUnitsCreatedTotal,
                    dataProviderName,
                    numberOfWorkUnitsCreated);
                
                if (numberOfWorkUnitsCreated > 0)
                {
                    IncrementCounterWithBase(
                        PerfCounterId.TimeToCreateWorkUnitAverage,
                        PerfCounterId.TimeToCreateWorkUnitBase,
                        dataProviderName,
                        ticks / numberOfWorkUnitsCreated);
                }
            }
            catch (Exception ex)
            {
                LogPerfCounterIncrementError(ex);
            }
        }

        
        public static void ImportedWorkUnit(
            string dataProviderName,
            long objectsInWorkUnit,
            long importTimeInTicks,
            long normalizationTimeInTicks,
            long objectsImported)
        {
            try
            {
                IncrementCounterWithBase(
                    PerfCounterId.ObjectsPerWorkUnitAverage,
                    PerfCounterId.ObjectsPerWorkUnitBase,
                    dataProviderName,
                    objectsInWorkUnit);

                if (objectsImported != 0)
                {
                    IncrementCounterWithBase(
                        PerfCounterId.TimeToImportObjectAverage,
                        PerfCounterId.TimeToImportObjectBase,
                        dataProviderName,
                        importTimeInTicks / objectsImported);

                    IncrementCounterWithBase(
                        PerfCounterId.TimeToNormalizeImportedObjectAverage,
                        PerfCounterId.TimeToNormalizeImportedObjectBase,
                        dataProviderName,
                        normalizationTimeInTicks / objectsImported);
                }

                IncrementDefaultAndInstanceBy(
                    PerfCounterId.ObjectsImportedTotal, 
                    dataProviderName, 
                    objectsImported);

                IncrementCounterWithBase(
                    PerfCounterId.TimeToImportWorkUnitAverage, 
                    PerfCounterId.TimeToImportWorkUnitBase, 
                    dataProviderName, 
                    importTimeInTicks);

                IncrementCounterWithBase(
                    PerfCounterId.ObjectsImportedPerWorkUnitAverage,
                    PerfCounterId.ObjectsImportedPerWorkUnitBase,
                    dataProviderName,
                    objectsImported);

                IncrementCounterWithBase(
                    PerfCounterId.TimeToNormalizeWorkUnitAverage, 
                    PerfCounterId.TimeToNormalizeWorkUnitBase,
                    dataProviderName,
                    normalizationTimeInTicks);

                IncrementDefaultAndInstance(PerfCounterId.WorkUnitsImportedTotal, dataProviderName);
            }
            catch (Exception ex)
            {
                LogPerfCounterIncrementError(ex);
            }
        }

        public static void TransferredWorkUnit(
            string dataProviderName, 
            long transferTimeInTicks,
            long workUnitSize)
        {
            try
            {
                IncrementCounterWithBase(
                    PerfCounterId.TimeToTransferWorkUnitAverage,
                    PerfCounterId.TimeToTransferWorkUnitBase,
                    dataProviderName,
                    transferTimeInTicks);

                IncrementDefaultAndInstance(PerfCounterId.WorkUnitsTransferredTotal, dataProviderName);

                IncrementCounterWithBase(
                    PerfCounterId.WorkUnitSizeAverage,
                    PerfCounterId.WorkUnitSizeBase,
                    dataProviderName,
                    workUnitSize);
            }
            catch (Exception ex)
            {
                LogPerfCounterIncrementError(ex);
            }
        }

        public static void WorkUnitsReceived(long receiveTimeInTicks)
        {
            try
            {
                IncrementCounterWithBase(
                    PerfCounterId.TimeToGetWorkUnitsAverage, 
                    PerfCounterId.TimeToGetWorkUnitsBase,
                    string.Empty, // We don't do this counter per-provider
                    receiveTimeInTicks);
            }
            catch (Exception ex)
            {
                LogPerfCounterIncrementError(ex);
            }
        }

        #endregion
    }
}
